#' Create a power transformation object
#'
#' This function can be used to create a proper trans object that encapsulates
#' a power transformation (x^n).
#'
#' @param n The degree of the power transformation
#'
#' @return A trans object
#'
#' @importFrom scales trans_new extended_breaks format_format
#' @importFrom MASS fractions
#'
#' @export
#'
#' @examples
#' # Power of 5 transformations
#' trans <- power_trans(2)
#' trans$transform(1:10)
#'
#' # Cubic root transformation
#' trans <- power_trans(1 / 3)
#' trans$transform(1:10)
#'
#' # Use it in a plot
#' ggplot() +
#'   geom_line(aes(x = 1:10, y = 1:10)) +
#'   scale_x_continuous(trans = power_trans(2),
#'                      expand = c(0, 1))
power_trans <- function(n) {
  trans_new(
    name = paste0('power of ', fractions(n)),
    transform = function(x) {
      x^n
    },
    inverse = function(x) {
      x^(1 / n)
    },
    breaks = extended_breaks(),
    format = format_format(),
    domain = c(0, Inf)
  )
}
#' Create radial data in a cartesian coordinate system
#'
#' This function creates a trans object that converts radial data to their
#' corresponding coordinates in cartesian space. The trans object is created for
#' a specific radius and angle range that will be mapped to the unit circle so
#' data doesn't have to be normalized to 0-1 and 0-2*pi in advance. While there
#' exists a clear mapping from radial to cartesian, the inverse is not true as
#' radial representation is periodic. It is impossible to know how many
#' revolutions around the unit circle a point has taken from reading its
#' coordinates. The inverse function will always assume that coordinates are in
#' their first revolution i.e. map them back within the range of a.range.
#'
#' @param r.range The range in radius that correspond to 0 - 1 in the unit
#' circle.
#'
#' @param a.range The range in angles that correspond to 2*pi - 0. As radians
#' are normally measured counterclockwise while radial displays are read
#' clockwise it's an inverse mapping
#'
#' @param offset The offset in angles to apply. Determines that start position
#' on the circle. pi/2 (the default) corresponds to 12 o'clock.
#'
#' @param pad Adds to the end points of the angle range in order to separate the
#' start and end point. Defaults to 0.5
#'
#' @param clip Should input data be clipped to r.range and a.range or be allowed
#' to extend beyond. Defaults to FALSE (no clipping)
#'
#' @return A trans object. The transform method for the object takes an r
#' (radius) and a (angle) argument and returns a data.frame with x and y columns
#' with rows for each element in r/a. The inverse method takes an x and y
#' argument and returns a data.frame with r and a columns and rows for each
#' element in x/y.
#'
#' @note While trans objects are often used to modify scales in ggplot2, radial
#' transformation is different as it is a coordinate transformation and takes
#' two arguments. Consider it a trans version of coord_polar and use it to
#' transform your data prior to plotting.
#'
#' @importFrom scales trans_new extended_breaks format_format
#'
#' @export
#'
#' @examples
#' # Some data in radial form
#' rad <- data.frame(r = seq(1, 10, by = 0.1), a = seq(1, 10, by = 0.1))
#'
#' # Create a transformation
#' radial <- radial_trans(c(0, 1), c(0, 5))
#'
#' # Get data in x, y
#' cart <- radial$transform(rad$r, rad$a)
#'
#' # Have a look
#' ggplot() +
#'   geom_path(aes(x = x, y = y), data = cart, color = 'forestgreen') +
#'   geom_path(aes(x = r, y = a), data = rad, color = 'firebrick')
radial_trans <- function(r.range, a.range, offset = pi / 2, pad = 0.5,
                         clip = FALSE) {
  a.range[which.min(a.range)] <- min(a.range) - pad
  a.range[which.max(a.range)] <- max(a.range) + pad
  trans_new(
    name = paste0(
      'radial-to-cartesian: ',
      r.range[1], '-', r.range[2], ' -> 0-1; ',
      a.range[1], '-', a.range[2], ' -> 2pi-0'
    ),
    transform = function(r, a) {
      if (clip) {
        r[r < min(r.range)] <- min(r.range)
        r[r > max(r.range)] <- max(r.range)
        a[a < min(a.range)] <- min(a.range)
        a[a > max(a.range)] <- max(a.range)
      }
      if (diff(r.range) == 0) {
        r <- 1
      } else {
        r <- (r - r.range[1]) / diff(r.range)
      }
      if (diff(a.range) == 0) {
        a <- offset
      } else {
        a <- offset + (a - a.range[1]) / diff(a.range) * -2 * pi
      }
      data.frame(x = r * cos(a), y = r * sin(a))
    },
    inverse = function(x, y) {
      r <- sqrt(x^2 + y^2) * diff(r.range) + r.range[1]
      angle <- -(atan2(y, x) - offset)
      angle[angle < 0] <- 2 * pi + angle[angle < 0]
      a <- angle / (2 * pi) * diff(a.range) + a.range[1]
      data.frame(r = r, a = a)
    },
    breaks = extended_breaks(),
    format = format_format()
  )
}
#' Reverse a transformation
#'
#' While the scales package export a reverse_trans object it does not allow for
#' reversing of already transformed ranged - e.g. a reverse exp transformation
#' is not possible. trans_reverser takes a trans object or something coercible
#' to one and creates a reverse version of it.
#'
#' @param trans A trans object or an object that can be converted to one using
#' [scales::as.trans()]
#'
#' @return A trans object
#'
#' @importFrom scales as.trans trans_new asn_trans atanh_trans boxcox_trans
#' date_trans exp_trans identity_trans log10_trans log1p_trans log2_trans
#' logit_trans log_trans probability_trans probit_trans reciprocal_trans
#' reverse_trans sqrt_trans time_trans
#'
#' @export
#'
#' @examples
#' # Lets make a plot
#' p <- ggplot() +
#'   geom_line(aes(x = 1:10, y = 1:10))
#'
#' # scales already have a reverse trans
#' p + scale_x_continuous(trans = 'reverse')
#'
#' # But what if you wanted to reverse an already log transformed scale?
#' p + scale_x_continuous(trans = trans_reverser('log'))
trans_reverser <- function(trans) {
  transformOrig <- as.trans(trans)
  trans_new(
    name = paste0('reverse-', transformOrig$name),
    transform = function(x) {
      -transformOrig$transform(x)
    },
    inverse = function(x) {
      transformOrig$inverse(-x)
    },
    breaks = transformOrig$breaks,
    format = transformOrig$format,
    domain = transformOrig$domain
  )
}
